// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: hyuan

#include "coin2/exchange/kucoin_futures/api_order/native_private_client.h"

#include <memory>
#include <sstream>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/kucoin/api_order/native_private_client.h"
#include "coin2/exchange/kucoin/api_util/rate_tracker.h"
#include "coin2/exchange/kucoin_futures/symbology/product.h"

using coin2::exchange::kucoin_futures::symbology::KucoinFuturesProduct;

#define CHECK_API_LIMIT(action_type, query_type)                                          \
  if (!local_rate_tracker_->HasEnoughRateLimit(ts, action_type, query_type)) {            \
    if (order_logger_cb_) {                                                               \
      rate_tracker_->WriteRateLimitReportMessage(order_logger_cb_);                       \
    }                                                                                     \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }

KucoinFuturesNativePrivateClient::KucoinFuturesNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  using coin2::exchange::kucoin::api_util::KucoinRateTracker;
  NativeClientConfig config;
  config.set_rest_host("api-futures.kucoin.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[KucoinFuturesNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<KucoinRestAuth, NativeClientConfig>(config);

  rate_tracker_ = std::make_unique<KucoinRateTracker>(mea_, config);
  local_rate_tracker_ = dynamic_cast<KucoinRateTracker*>(rate_tracker_.get());
  start_time_ = GetCurrentTimestamp();
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::QueryAccountBalance(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::BALANCE);

  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/account-overview", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::BALANCE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::QueryAccountPosition() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::POSITION);

  UrlParamsBuilder builder;
  auto context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/positions", &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_POSITION;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::QueryOpenOrders() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::OPEN_ORDER);

  UrlParamsBuilder builder;
  builder.PutGet("status", "active");
  builder.PutGet("currentPage", "1");
  builder.PutGet("pageSize", "1000");
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/orders", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::OPEN_ORDER);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::QueryHistoryOrders() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::HIST_ORDER);

  UrlParamsBuilder builder;
  builder.PutGet("status", "done");
  builder.PutGet("currentPage", "1");
  builder.PutGet("pageSize", "1000");
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/orders", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::HIST_ORDER);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::QueryFills() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::FILL_ORDER);

  UrlParamsBuilder builder;
  builder.PutGet("currentPage", "1");
  builder.PutGet("pageSize", "1000");
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/fills", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::FILL_ORDER);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::QueryBullet() {
  if (is_bullet_ready_) {
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::UNKNOWN);

  UrlParamsBuilder builder;
  auto context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/bullet-private", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::UNKNOWN);
  api_req_info_[__func__]++;

  // context->query_type = KucoinOrderSubTopicId::REST_BULLET;
  context->query_type = OrderSubTopicId::LISTEN_KEY;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

void KucoinFuturesNativePrivateClient::SetBullet(const std::string& channel, void* data) {
  std::vector<std::string>* bullet_info = static_cast<std::vector<std::string>*>(data);
  if (bullet_info->size() == 5) {
    is_bullet_ready_ = true;
  }
}

void KucoinFuturesNativePrivateClient::Prepare() {
  mb_->Subscribe(
      "kucoin/bullet",
      std::bind(
          &KucoinFuturesNativePrivateClient::SetBullet,
          this,
          std::placeholders::_1,
          std::placeholders::_2));
  mb_->Subscribe("kucoin/enable_time", [this](const std::string&, void* data) {
    int64_t value = reinterpret_cast<int64_t>(data);
    local_rate_tracker_->UpdateEnableTime(value);
    LOG(WARNING) << "Kucoin futures native client enable_time: " << value;
  });
}

std::string KucoinFuturesNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;
  std::stringstream ss;
  ss << "KucoinFuturesNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

auto KucoinFuturesNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();
  KucoinFuturesProduct product =
      KucoinFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  params.set_symbol(order_spec.product);
  params.set_client_order_id(std::to_string(proc_order_id));
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }
  auto formatter = product_holder->product_info().order_formatter();
  params.set_size(formatter.FormatQty(order_spec.qty));
  if (order_spec.order_type != OrderType::MARKET_ORDER) {
    params.set_price(formatter.FormatPrice(order_spec.price));
  }
  if (order_spec.order_type == OrderType::LIMIT_ORDER) {
    params.set_type("limit");
    if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
      params.set_time_in_force("IOC");
    } else {
      params.set_time_in_force("GTC");
      params.set_post_only(order_spec.post_only ? "true" : "false");
    }
  } else if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_type("market");
  } else {
    NOTREACHED() << "Unknown order type: " << static_cast<int>(order_spec.order_type);
  }
  params.set_leverage(order_spec.leverage_rate);

  return params;
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::PlaceOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::PLACE, QueryType::UNKNOWN);

  UrlParamsBuilder builder;
  builder.PutPost("clientOid", params.client_order_id());
  builder.PutPost("side", params.side());
  builder.PutPost("symbol", params.symbol());
  builder.PutPost("type", params.type());
  builder.PutPost("size", params.size());
  if (params.type() == "limit") {
    builder.PutPost("price", params.price());
  }
  builder.PutPost("timeInForce", params.time_in_force());
  builder.PutPost("postOnly", params.post_only());
  builder.PutPost("leverage", params.leverage());

  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/orders", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::PLACE, QueryType::UNKNOWN);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

auto KucoinFuturesNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    // TODO(hyuan): Further test is required.
    CHECK(false) << "[GetRestCancelParams] Order ID is required";
    params.set_client_order_id(std::to_string(order_context.proc_order_id()));
  }

  return params;
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::CancelOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::CANCEL, QueryType::UNKNOWN);

  UrlParamsBuilder builder;
  std::string path = "/api/v1/orders/";
  if (params.has_order_id()) {
    path = path + params.order_id();
  } else if (params.has_client_order_id()) {
    path = path + params.client_order_id();
  } else {
    NOTREACHED() << "No order id or client order id found";
    return {};
  }

  auto* context = GetAuth()->CreateRequestByDeleteWithSignature(path, &builder);
  local_rate_tracker_->Push(1, ts, ActionType::CANCEL, QueryType::UNKNOWN);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> KucoinFuturesNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}
